# 残差网络

"""
步骤：
1、准备数据集，包括训练集和测试集
    1.1、指定数据集 datasets
    1.2、加载数据 DatasetLoader
2、定义网络模型结构，继承nn.Module
3、定义损失函数和优化器
4、使用数据训练和测试模型
"""

import torch
import torch.nn as nn
# 定义数据加载器
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader

# batch 大小
batch_size = 64

# 1、构建数据集
# 数据转化
transform = transforms.Compose([
    # 转化为tensor
    transforms.ToTensor(),
    # 归一化，这个数是利用这个数据集计算出来的，可以提前手动计算，也可以使用程序自动计算
    transforms.Normalize((0.1307,), (0.3081,))
])

# root 指定下载位置，train 是否是训练集， download 如果数据集不存在是否下载，transform 转换方式
train_dataset = datasets.MNIST(root='../../datasets/mnist',
                               train=True,
                               download=True,
                               transform=transform)

# 数据加载器，dataset 加载的数据集，batch_size 训练时你 batch 的大小，shuffle 是否打乱顺序
train_loader = DataLoader(dataset=train_dataset,
                          batch_size=batch_size,
                          shuffle=True,
                          num_workers=2)

# 定义测试集和加载测试集数据
test_dataset = datasets.MNIST(root='../../datasets/mnist/',
                              train=False,
                              download=True,
                              transform=transform)
test_loader = DataLoader(dataset=test_dataset,
                         # 测试集通常不需要打乱顺序
                         shuffle=False,
                         batch_size=batch_size)


# 残差块
class ResidualBlock(nn.Module):
    def __init__(self, channels):
        super(ResidualBlock, self).__init__()
        self.channels = channels
        self.conv1 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(channels, channels, kernel_size=3, padding=1)
        self.activate = nn.ReLU()

    def forward(self, x):
        y = self.activate(self.conv1(x))
        y = self.conv2(y)
        return self.activate(x + y)


# 网络模型
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 16, kernel_size=5)
        self.conv2 = nn.Conv2d(16, 32, kernel_size=5)
        self.mp = nn.MaxPool2d(2)
        self.rblock1 = ResidualBlock(16)
        self.rblock2 = ResidualBlock(32)
        self.fc = nn.Linear(512, 10)
        self.activate = nn.ReLU()

    def forward(self, x):
        in_size = x.size(0)
        x = self.mp(self.activate(self.conv1(x)))
        x = self.rblock1(x)
        x = self.mp(self.activate(self.conv2(x)))
        x = self.rblock2(x)
        x = x.view(in_size, -1)
        x = self.fc(x)
        return x


# 获得模型实例
model = Net()
# 判断是否使用GPU加速 "cuda:0" 表示GPU的下标
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)

# 3、定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.5)


# 4、训练
def train(epoch):
    running_loss = 0.0
    for batch_idx, data in enumerate(train_loader, 0):
        # 这里 inputs 就是一个batch
        inputs, target = data
        # 把数据也放到对应的设备上 device
        inputs, target = inputs.to(device), target.to(device)
        # 梯度清 0
        optimizer.zero_grad()
        # forward + backward + update
        outputs = model(inputs)
        # 计算交叉熵损失
        loss = criterion(outputs, target)
        # 反向传播计算梯度
        loss.backward()
        # 优化器更新参数
        optimizer.step()
        # 用于输出，这是个可选操作
        running_loss += loss.item()
        # 每 300 次输出一次计算结果
        if batch_idx % 300 == 299:
            print('[%d, %5d] loss: %.3f' % (epoch + 1, batch_idx + 1, running_loss / 2000))
            running_loss = 0.0


# 测试操作
def test():
    # 预测正确的样本数
    correct = 0
    # 总的样本数
    total = 0
    # with torch.no_grad(): 模块包含的语句不再计算梯度
    with torch.no_grad():
        for data in test_loader:
            inputs, target = data
            inputs, target = inputs.to(device), target.to(device)
            outputs = model(inputs)
            # 这个函数返回的是两个值，第一个值是具体的value（我们用下划线_表示），第二个值是value所在的index（也就是predicted，预测的标签，因为这里最大值的索引就是标签）
            _, predicted = torch.max(outputs.data, dim=1)
            # print(_, predicted)
            total += target.size(0)
            # 这里 (predicted == labels).sum().item() 返回两个数组中相同的下标的元素个数，就是预测值和标签值相等的个数
            correct += (predicted == target).sum().item()
        print('Accuracy on test set: %d %% [%d/%d]' % (100 * correct / total, correct, total))


if __name__ == '__main__':
    for epoch in range(10):
        train(epoch)
        test()
